﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using DependencyAnalysis.DataModel;

namespace DependencyAnalysis.Internal
{
    public class LiskovSubstitutionPrinciple
    {
        public IDependencyModel Model { get; set; }

        public LiskovSubstitutionPrinciple(IDependencyModel model)
        {
            Model = model;
        }

        public IEnumerable<MemberDescriptor> SelectViolatingMembers(IEnumerable<MemberDescriptor> members)
        {
            foreach (var member in members)
            {
                if (member.MemberType != MemberType.Method) continue;
                var overriddenMember = FindOverriddenMember(member);
                if (overriddenMember == null) continue;
                if (!member.IsOverride && !member.IsDefinedByInterface) yield return member;
                if (!member.IsOverride || member.IsDefinedByInterface || overriddenMember.IsAbstract) continue;
                if (MemberViolatesPrinciple(member, overriddenMember)) yield return member;
            }
        }

        private bool MemberViolatesPrinciple(MemberDescriptor member, MemberDescriptor overriddenMember)
        {
            if (!Model.DependenciesPerMemberOut[member].Any(x => Equals(x.Target, overriddenMember))) return true;
            var instructions = Model.InstructionCache.GetInstructions(member);
            long indexBaseCall = FindBaseCallIndex(overriddenMember, instructions);
            long indexFirstReturn = FindFirstReturnIndex(instructions);
            if (indexFirstReturn < indexBaseCall) return true;
            if (ModifiesBaseField(member, Model.InstructionCache)) return true;
            if (!member.Signature.StartsWith("Void ") && ReturnValueIsChanged(instructions, indexBaseCall)) return true;
            if (MemberThrowsNewExceptionType(member, overriddenMember)) return true;
            return false;
        }

        private bool MemberThrowsNewExceptionType(MemberDescriptor member, MemberDescriptor overriddenMember)
        {
            var baseExceptions = new HashSet<Type>();
            var currentExceptions = new HashSet<Type>();
            AddExceptionsToSet(overriddenMember, baseExceptions, true);
            AddExceptionsToSet(member, currentExceptions, true);
            return currentExceptions.Any(currentException =>
                !baseExceptions.Any(x => x.IsAssignableFrom(currentException)));
        }

        private void AddExceptionsToSet(MemberDescriptor member, HashSet<Type> exceptions, bool includeCalls)
        {
            foreach (var efference in Model.DependenciesPerMemberOut[member])
            {
                var targetMember = efference.Target;
                if (targetMember.Name == ".ctor" && typeof(Exception).IsAssignableFrom(targetMember.ImplementingType.Type))
                {
                    exceptions.Add(targetMember.ImplementingType.Type);
                    continue;
                }
                if (includeCalls && efference.Target.MemberType == MemberType.Method &&
                    member.Signature != efference.Target.Signature)
                {
                    AddExceptionsToSet(efference.Target, exceptions, false);
                }
            }
        }

        private static bool ReturnValueIsChanged(IEnumerable<Instruction> instructions, long indexBaseCall)
        {
            var validCopies = new HashSet<int>();
            Instruction lastInstruction = null;
            foreach (var instruction in instructions)
            {
                var currentOpCode = instruction.OpCode;
                if (currentOpCode == OperationCode.Nop) continue;

                if (currentOpCode == OperationCode.Ret && lastInstruction != null
                    && lastInstruction.Offset != indexBaseCall)
                {
                    if (InstructionCache.ArithmethicInstructions.Contains(lastInstruction.OpCode) ||
                        InstructionCache.TypeOpCodes.Contains(lastInstruction.OpCode))
                    {
                        return true;
                    }

                    if (InstructionCache.MethodOpCodes.Contains(lastInstruction.OpCode))
                    {
                        var methodInfo = lastInstruction.OperandNonSerialized as MethodInfo;
                        if (methodInfo != null && methodInfo.ReturnType != typeof(void))
                        {
                            return true;
                        }
                    }

                    ushort lastLoadlocIndex = GetLoadLocIndex(lastInstruction, instruction);
                    if (!validCopies.Contains(lastLoadlocIndex))
                    {
                        return true;
                    }

                }

                var storeLocIndex = GetStoreLocIndex(currentOpCode, instruction);
                if (lastInstruction != null)
                {
                    if (lastInstruction.Offset == indexBaseCall)
                    {
                        validCopies.Add(storeLocIndex);
                    }
                    else
                    {
                        var loadLocIndex = GetLoadLocIndex(lastInstruction, instruction);
                        if (loadLocIndex != ushort.MaxValue
                            && storeLocIndex != ushort.MaxValue
                            && validCopies.Contains(loadLocIndex))
                        {
                            validCopies.Add(storeLocIndex);
                        }
                    }
                }
                lastInstruction = instruction;
            }
            return false;
        }

        private static ushort GetLoadLocIndex(Instruction lastInstruction, Instruction instruction)
        {
            ushort ldlocIndex = ushort.MaxValue;
            switch (lastInstruction.OpCode)
            {
                case OperationCode.Ldloc_0:
                    ldlocIndex = 0;
                    break;
                case OperationCode.Ldloc_1:
                    ldlocIndex = 1;
                    break;
                case OperationCode.Ldloc_2:
                    ldlocIndex = 2;
                    break;
                case OperationCode.Ldloc_3:
                    ldlocIndex = 3;
                    break;
                case OperationCode.Ldloc:
                    ldlocIndex = (ushort)instruction.OperandNonSerialized;
                    break;
            }
            return ldlocIndex;
        }

        private static ushort GetStoreLocIndex(OperationCode currentOpCode, Instruction instruction)
        {
            ushort stlocIndex = ushort.MaxValue;
            switch (currentOpCode)
            {
                case OperationCode.Stloc_0:
                    stlocIndex = 0;
                    break;
                case OperationCode.Stloc_1:
                    stlocIndex = 1;
                    break;
                case OperationCode.Stloc_2:
                    stlocIndex = 2;
                    break;
                case OperationCode.Stloc_3:
                    stlocIndex = 3;
                    break;
                case OperationCode.Stloc:
                    stlocIndex = (ushort)instruction.OperandNonSerialized;
                    break;
            }
            return stlocIndex;
        }

        private bool ModifiesBaseField(MemberDescriptor member, InstructionCache instructionCache)
        {
            var instructions = instructionCache.GetInstructions(member);
            var inheritedTypes = member.ImplementingType.InheritedTypes;
            var ownMethodCalls = instructions.Where(instruction => InstructionCache.MethodOpCodes.Contains(instruction.OpCode)).Select(instruction => DescriptorBuilder.BuildMemberDescriptor((MemberInfo)instruction.OperandNonSerialized)).Where(memberDescriptor => inheritedTypes.Contains(memberDescriptor.ImplementingType)).ToList();
            ownMethodCalls.Add(member);
            return Methodenreinheit.MethodModifiesField(instructionCache, ownMethodCalls, inheritedTypes);
        }

        private static long FindFirstReturnIndex(IList<Instruction> instructions)
        {
            long indexFirstReturn = long.MaxValue;
            var lastOffset = instructions.Last().Offset;
            foreach (var instruction in instructions)
            {
                if (instruction.OpCode == OperationCode.Ret ||
                    instruction.OpCode == OperationCode.Throw ||
                    instruction.OpCode == OperationCode.Rethrow)
                {
                    indexFirstReturn = instruction.Offset;
                    break;
                }
                if (InstructionCache.NestingInstructions.Contains(instruction.OpCode))
                {
                    var operand = (long)instruction.OperandNonSerialized;
                    if (operand >= lastOffset)
                    {
                        indexFirstReturn = instruction.Offset;
                        break;
                    }
                }
            }
            return indexFirstReturn;
        }

        private static long FindBaseCallIndex(MemberDescriptor overriddenMember, IList<Instruction> instructions)
        {
            long indexBaseCall = long.MaxValue;
            foreach (var instruction in instructions)
            {
                if (InstructionCache.MethodOpCodes.Contains(instruction.OpCode))
                {
                    var memberDescriptor = DescriptorBuilder.BuildMemberDescriptor((MethodBase)instruction.OperandNonSerialized);
                    if (Equals(overriddenMember, memberDescriptor))
                    {
                        indexBaseCall = instruction.Offset;
                        break;
                    }
                }
            }
            return indexBaseCall;
        }

        private static MemberDescriptor FindOverriddenMember(MemberDescriptor member)
        {
            var type = member.ImplementingType;
            var signature = member.Signature;
            foreach (var parent in type.InheritedTypes)
            {
                var baseMember = parent.Members.FirstOrDefault(x => Equals(signature, x.Signature));
                if (baseMember != null) return baseMember;
            }
            return null;
        }
    }
}
